......的確,我現在一想起來,這整件事情好像真的有點騎士精神的味道。不過,我全部的騎士精神從開始到結束一轉眼就過了,不然騎士就糟了。
-- <小英雄(摘自來歷不明的回憶錄)>,杜斯妥也夫斯基著,丘光譯
多病多愁,須信從來錯。
-- <醉落魄,席上呈元素>,蘇軾詞
若要說這一套遊戲引擎的核心要素是什麼,那麼我會說是 Action
資料結構。它的設計理念是,它可以用來表示整個標準回合的內容,以及表示當中任何階段的內容。
為什麼?一個 Board
資料結構,難道不足以表達嗎?的確,本來在專案之初,我也沒有想到需要這個中間抽象層的設計。但是在真正要銜接 model 訓練的時候,我遭遇到一個沒有選擇餘地的處境。這需要一些解釋。
訓練的目的,就是希望一個原本沒有任何知識的代理人,能夠在給定的情境,做出對應的決策,而且決策的品質能夠越來越高(這也就是強化學習當中的強化的意義)。在疫途這個遊戲裡面,一個決策,應該是怎樣的形式?我當初沒有糾結在這個問題太久,因為加上我手邊的參考書,技術上我只知道迴歸可以針對單一數值的答案、分類可以針對一群候選答案或行動的分佈,以學習;然而,我的武器庫裡面並沒有明顯的方法可以訓練神經網路來針對不特定長度的序列學習。
所以回到疫途的術語,這表示我沒辦法奢求情境 -> 標準回合這種方便的東西,因為疫途的任何標準回合可能由 1 個到 13 個不等的著手構成。對於 DeltaPathogen 專案來說,足夠好的目標是訓練出一個代理人,讓它能夠掌握一個效果不錯的情境 -> 座標的通用函數。這裡的情境,也就代表不只是玩家切換的時候、一個標準回合結束、新的標準回合正要開始的棋局狀態,它必須也能呈現一個標準回合中的任何數量的著手已經被決定的時候的狀態。它被期待能夠,針對那些情境給出一個局勢判斷,然後給出可以考慮的著點座標的優劣分布。
然而,這些也還不完全是需要這個資料結構的理由。
回想第三日,我提及一種已經放棄了的選擇題式的代理人構想,它在技術上代表一個挑戰,而這挑戰又是直接根生自規則的。在一個標準回合內雖然有多重著手的座標選點,但它們都必須符合自洽的邏輯。人類遊玩時,他們當然會謀定而後動而不會造成邏輯無法自洽的錯誤(其實就算否,人類主控的遊戲系統的韌性非常強大,不難恢復)。但是我們的隨機猴子當然沒有內在概念,有的只是缺乏前後連貫的隨機試誤,當然沒有保證能夠順暢、合規地行棋。昨天的介紹應該已經建立了這個形象,這裡就不贅述。
運行遊戲系統(遊戲引擎容易誤導)最重要的就是確保遊戲狀態的一致。然而承上,沒有遊戲概念的隨機猴子,乃至於我們可以預先料想的、早期訓練出來的的顢頇代理人,就會像是開車迷路之人,可能開到一處死巷,再只好一路回溯至前一個分岔路口。它可能會決定一個實際上不可走的羅盤選點,但在棋盤階段走了幾步之後才發現不通,只好重選;或是,在棋盤階段也走不到正確的下一格(畢竟隨機選擇座標沒有連續性,當然也可以作成有,但疫途的人界、冥界領域分野造成這件事情也有難度)。屆時,為了讓這個回溯的過程輕鬆些,Action 類別才因此誕生。
也就是說,標準回合內的任何階段,都不會直接更新到棋盤上,而是以**(在一個標準回合內不改變(immutable))的棋盤加上隨各階段更新的 Action 物件**兩者共同維護每個階段的狀態,而且易於回復:直接砍掉 Action 即可。
然而,立意雖佳,實務上,只有這個機制的話,效能還是不能接受的。且於明日補充。
commit_action
至於順利完成了一個 Action
時,就表示有一個合乎規則的標準回合完成了,且可以對遊戲盤面造成改變。關鍵函數如下列出:
pub fn commit_action(&mut self, a: &Action) {
if let Phase::End(_x) = self.phase {
panic!("The game is finished. Not expected here.");
}
if a.lockdown != Lockdown::Normal {
let c_start = *self.map.get(&Camp::Plague).unwrap();
self.set_map(Camp::Plague, c_start.lockdown(a.lockdown));
}
if a.map != None {
self.set_map(self.turn, a.map.unwrap());
} else {
// been skipped
self.check_and_set_end();
return;
}
self.character
.insert((a.world.unwrap(), self.turn), a.character.unwrap());
let m = a.markers.clone();
let t = self.turn;
for c in m.iter() {
self.add_marker(c, &t);
}
self.check_and_set_end();
}
這個函數定義在 Struct Game
裡面,所以 self
指的是這局遊戲。根據 self.phase
區分階段,判定不是結束的狀態(self.phase == Phase::End(_x)
,其中前置底線是 Rust 建議使用者,標記未使用變數的慣例;End
狀態本身是可以不需要附帶一個數值(這裡的 _x
),但是我在操作過程中可能會需要調整棋局歷史,所以這裡維護的是遊戲總步數)。如果不是的話,那就按照 map
與 lockdown
兩個成員變數來更新羅盤階段;可以發現的是這裡沒有棋盤階段的那些資訊,因為角色(a.character
)已經走到它的目的地了。然後透過 self.add_marker
補上標記階段。最後以 check_and_set_end
判斷棋局是否已經結束。
已經將 tsume_generator
投入應用之中,但是還需要點時間來收集訓練的結果。還是趁週末趕緊累積先前規劃的主題吧...